/*
This file is part of jasima, the Java simulator for manufacturing and logistics.
 
Copyright 2010-2022 jasima contributors (see license.txt)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jasima.shopSim.models.dynamicShop;

import static jasima.core.util.i18n.I18n.defFormat;

import java.util.Arrays;
import java.util.Objects;

import org.apache.commons.math3.distribution.ExponentialDistribution;

import jasima.core.random.continuous.DblConst;
import jasima.core.random.continuous.DblDistribution;
import jasima.core.random.continuous.DblSequence;
import jasima.core.random.discrete.IntUniformRange;
import jasima.core.simulation.arrivalprocess.ArrivalsStationary;
import jasima.core.util.Pair;
import jasima.core.util.TypeUtil;
import jasima.core.util.Util;
import jasima.shopSim.core.DynamicJobSource;
import jasima.shopSim.core.DynamicJobSourceOldStreamNames;
import jasima.shopSim.core.Job;
import jasima.shopSim.core.JobSource;
import jasima.shopSim.core.Operation;
import jasima.shopSim.core.Shop;
import jasima.shopSim.core.ShopExperiment;
import jasima.shopSim.core.ShopListener;
import jasima.shopSim.core.ShopListenerBase;
import jasima.shopSim.core.WorkStation;
import jasima.shopSim.util.BasicJobStatCollector;

/**
 * Simulates dynamic job shops and flow shops, based on some parameters. See
 * Rajendran, C.; Holthaus O.: "A Comparative Study of Dispatching Rules in
 * Dynamic Flowshops and Jobshops", European Journal of Operational Research 116
 * (1999) 1, S. 156-170 for details.
 * <p>
 * An experiment of this type by default contains a
 * {@code BasicJobStatCollector}.
 * 
 * @author Torsten Hildebrandt
 * @see BasicJobStatCollector
 */
@SuppressWarnings("deprecation")
public class DynamicShopExperiment extends ShopExperiment {

	private static final long serialVersionUID = -7289579158397939550L;

	public enum Scenario {
		JOB_SHOP, FLOW_SHOP
	};

	private static final double MINUTES_PER_DAY = 24 * 60;

	private double utilLevel = 0.85d;
	private DblSequence dueDateFactor = new DblConst(4.0d);
	private int numMachines = 10;
	private Scenario scenario = Scenario.JOB_SHOP;
	private DblSequence weights = null;
	private Pair<Integer, Integer> numOps = new Pair<Integer, Integer>(-1, -1);
	private DblSequence procTimes = new IntUniformRange(1, 49);
	private int stopArrivalsAfterNumJobs = 2500;

	protected JobSource src;

	public DynamicShopExperiment() {
		super();
		addShopListener(new BasicJobStatCollector());
	}

	@Override
	public void init() {
		super.init();

		if (getScenario() == null)
			throw new IllegalArgumentException(
					defFormat("No scenario specified, should be one of %s.", Arrays.toString(Scenario.values())));

		Objects.requireNonNull(procTimes);

		ShopListener stopSrc = new ShopListenerBase() {
			int maxJob = getStopArrivalsAfterNumJobs();
			int numJobs = maxJob;

			@Override
			public void jobFinished(Shop shop, Job j) {
				// stop arrivals after the first, e.g., 2500, jobs were
				// completed
				if (j.getJobNum() < maxJob) {
					if (--numJobs == 0) {
						src.stopArrivals = true;
					}
				}
			}
		};
		shop.addListener(stopSrc);
	}

	@Override
	protected void createShop() {
		super.createShop();

		createMachines();

		src = createJobSource();
		shop.addJobSource(src);
	}

	@Override
	protected void configureShop() {
		super.configureShop();

		if (getStopAfterNumJobs() <= 0)
			shop.setStopAfterNumJobs(10 * getStopArrivalsAfterNumJobs());
	}

	private void createMachines() {
		for (int n = 0; n < getNumMachines(); n++) {
			WorkStation m = new WorkStation(1);
			shop.addMachine(m);
		}
	}

	protected JobSource createJobSource() {
		DynamicJobSource src = new DynamicJobSourceOldStreamNames() {

			@Override
			protected Operation[] createRoute() {
				final int n = getNumOps().nextInt();
				assert n > 0;

				Operation[] ops = Util.initializedArray(n, Operation.class);

				// initially all false
				boolean[] machineChosen = new boolean[getNumMachines()];

				for (int i = 0; i < n; i++) {
					int mi = -1;
					do {
						mi = getMachIdx().nextInt();
					} while (machineChosen[mi]);

					WorkStation m = (WorkStation) shop.machines().getChild(mi);
					machineChosen[mi] = true;

					if (getScenario() == Scenario.JOB_SHOP) {
						ops[i].setMachine(m);
					}
				}

				if (getScenario() == Scenario.FLOW_SHOP) {
					int k = 0;
					for (int i = 0, j = shop.machines().numChildren(); i < j; i++) {
						if (machineChosen[i])
							ops[k++].setMachine((WorkStation) shop.machines().getChild(i));
					}
				}

				// procTimes
				for (Operation o : ops) {
					o.setProcTime(getProcTimes().nextDbl());
				}

				return ops;
			}
		};

		double iaMean = calcIaMean();

		ArrivalsStationary arrivals = new ArrivalsStationary();
		arrivals.setInterArrivalTimes(new DblDistribution(new ExponentialDistribution(iaMean)));
		src.setArrivalProcess(arrivals);

		int min = getNumOpsMin() > 0 ? getNumOpsMin() : getNumMachines();
		int max = getNumOpsMax() > 0 ? getNumOpsMax() : getNumMachines();
		if (min > max)
			throw new IllegalArgumentException(
					defFormat("invalid range for numOps: [%d; %d]", getNumOpsMin(), getNumOpsMax()));
		if (max > getNumMachines())
			throw new IllegalArgumentException(
					defFormat("Can't have more operations (%d) than there are machines (%d).", max, getNumMachines()));
		IntUniformRange numOps = new IntUniformRange(min, max);
		src.setNumOps(numOps);

		DblSequence procTimes2 = TypeUtil.cloneIfPossible(getProcTimes());
		src.setProcTimes(procTimes2);

		src.setMachIdx(new IntUniformRange(0, getNumMachines() - 1));

		src.setDueDateFactors(TypeUtil.cloneIfPossible(getDueDateFactor()));

		if (getWeights() != null) {
			src.setJobWeights(getWeights().clone());
		}

		return src;
	}

	@Override
	protected void done() {
		// was simulation stopped early?
		aborted = (shop.jobsStarted - shop.jobsFinished) > 0 ? 1 : 0;

		super.done();
	}

	private double calcIaMean() {
		int opsMin = getNumOpsMin() > 0 ? getNumOpsMin() : getNumMachines();
		int opsMax = getNumOpsMax() > 0 ? getNumOpsMax() : getNumMachines();

		double meanOps = 0.5d * (opsMax + opsMin);
		double meanOpProc = getProcTimes().getNumericalMean();

		double jobsPerDay = getUtilLevel() * getNumMachines() * MINUTES_PER_DAY / (meanOps * meanOpProc);
		return (1.0d * MINUTES_PER_DAY / jobsPerDay);
	}

	public double getUtilLevel() {
		return utilLevel;
	}

	/**
	 * Sets the desired utilization level for all machines. Machine utilization
	 * approaches this value in the long term; short term results might differ due
	 * to random influences in the arrival process.
	 */
	public void setUtilLevel(double utilLevel) {
		if (utilLevel < 0.0d || utilLevel > 1.0d)
			throw new IllegalArgumentException("" + utilLevel);

		this.utilLevel = utilLevel;
	}

	public DblSequence getDueDateFactor() {
		return dueDateFactor;
	}

	/**
	 * Sets the due date tightness of jobs by specifying a due date factor. The
	 * {@link DblSequence} is used to calculate a job's due date as a multiple of a
	 * job's processing time. If for instance a due date factor of 2 is returned for
	 * a certain job then the due date is set to the job's release date plus twice
	 * the raw processing time of all operations of this job.
	 */
	public void setDueDateFactor(DblSequence dueDateFactor) {
		this.dueDateFactor = dueDateFactor;
	}

	public int getNumMachines() {
		return numMachines;
	}

	/**
	 * Sets the number of machines on the shop floor.
	 */
	public void setNumMachines(int numMachines) {
		if (numMachines < 1)
			throw new IllegalArgumentException("" + numMachines);

		this.numMachines = numMachines;
	}

	/** Returns the minimum number of operations of a job. */
	public int getNumOpsMin() {
		return numOps.a;
	}

	/**
	 * Sets the minimum number of operations of a job. Setting this to a value
	 * {@code <=0} uses the number of machines, i.e., each job has to visit each
	 * machine exactly once.
	 */
	public void setNumOpsMin(int min) {
		numOps = new Pair<Integer, Integer>(min, numOps.b);
	}

	/**
	 * Returns the maximum number of operations of a job. Setting this to a value
	 * {@code <=0} uses the number of machines, i.e., a job with the maximum number
	 * of operations has to visit each machine exactly once.
	 */
	public int getNumOpsMax() {
		return numOps.b;
	}

	/** Sets the maximum number of operations of a job. */
	public void setNumOpsMax(int max) {
		numOps = new Pair<Integer, Integer>(numOps.a, max);
	}

	public void setNumOps(int min, int max) {
		if (min < 0 || (max < min))
			throw new IllegalArgumentException("[" + min + ";" + max + "]");
		numOps = new Pair<Integer, Integer>(min, max);
	}

	/**
	 * Sets the scenario to use. This can be either {@code JOB_SHOP} or
	 * {@code FLOW_SHOP}.
	 */
	public void setScenario(Scenario scenario) {
		this.scenario = scenario;
	}

	public Scenario getScenario() {
		return scenario;
	}

	public int getStopArrivalsAfterNumJobs() {
		return stopArrivalsAfterNumJobs;
	}

	/**
	 * The job source is stopped after a certain number of jobs were completed. Jobs
	 * are counted in the order they entered the system. If, e.g.,
	 * {@code stopAfterNumJobs} is 2500 the job source is stopped after all of the
	 * first 2500 jobs were completed (note: this is is not necessarily the same as
	 * the first 2500 jobs completed).
	 * 
	 * @param stopAfterNumJobs The number of jobs after which to stop, default:
	 *                         2500.
	 */
	public void setStopArrivalsAfterNumJobs(int stopAfterNumJobs) {
		this.stopArrivalsAfterNumJobs = stopAfterNumJobs;
	}

	public DblSequence getWeights() {
		return weights;
	}

	/**
	 * Sets the weights to be used for each job. The default setting is to assign a
	 * weight of 1 for each job when this attribute is {@code null}.
	 * 
	 * @param weights A {@link DblSequence} to determine job weight. Default: each
	 *                job gets a weight of 1.
	 */
	public void setWeights(DblSequence weights) {
		this.weights = weights;
	}

	public DblSequence getProcTimes() {
		return procTimes;
	}

	/**
	 * Determines the processing times for each operation. This is a mandatory
	 * setting.
	 */
	public void setProcTimes(DblSequence procTimes) {
		this.procTimes = procTimes;
	}

}
